In this lesson, we will take the manual, toilsome process of publishing a new release and transform it into GitHub workflow automation triggered by pushing a tag to the repository. This automation will result in a GitHub release containing build notes and release artifacts for a tagged, semantic version of the tweeter command-line tool. Automating manual processes such as releases reduces the possibility of manual errors and increases the productivity of project maintainers.

svg viewer

In this lesson, we will learn how to create a release automation workflow. We will learn how to trigger an automation to run after the successful completion of dependent automation. We will learn how to build binaries targeting multiple platforms. Finally, we will automate the creation of a GitHub release, including automatically generated release notes.

GitHub releases#

GitHub releases are deployable software iterations for a repository that are based on Git tags. A release declares to the world that a new version of the software is available. A release is composed of a title, an optional description, and an optional set of artifacts. The title provides a name for the release. The description is used to provide insight into what is contained in the release – for example, what new features or pull requests were included in the release, and which GitHub contributors contributed to the release.

svg viewer

The description is formatted in GitHub Markdown. Release artifacts are files associated with the release that users can download – for example, a command-line application might publish compiled binaries ready for download and use.

Git tags#

A Git tag is a named pointer to a specific reference in the Git repository and is often formatted as semantic versions, such as v1.2.3. Semantic versioning is a convention for naming tags that provides some insight into the significance of a new release. A semantic version tag is formatted as Major.Minor.Patch. The following behavior is expressed by incrementing the individual field:

  • Major: Increment when incompatible API changes occur, such as breaking changes.

  • Minor: Increment when functionality is added in a backward-compatible manner, such as new features.

  • Patch: Increment when making backward-compatible bug fixes.

Release automation for tweeter#

We will build upon the CI automation and add release automation for tweeter.

Goals for automation#

In our release automation, we are going to accomplish the following goals:

  • Trigger automation when the repository is tagged with a semantic version.

  • Run unit tests and validation prior to creating the release.

  • Inject the semantic version of the release into the tweeter application.

  • Build cross-platform versions of the tweeter application.

  • Generate release notes from the pull requests in the release.

  • Tag the contributors in the release.

  • Create a GitHub release containing the following:

    • A title containing the semantic version of the release.

    • A description containing the generated release notes.

    • Artifacts consisting of the cross-platform binaries.

Next, we will create release automation to satisfy these requirements.

Creating the release automation#

With our goals for the tweeter release automation specified, we are ready to extend the existing continuous integration workflow that we built in the previous section and add a release job to achieve those goals. The release job is longer than the continuous integration workflow, so we'll approach it one piece at a time.

Triggering the automation#

The first goal for the tweeter release workflow is triggering the automation when the repository is tagged with a semantic version:

Triggering the tweeter-automation with tags and push/pull

The preceding snippet of YAML is unchanged from the continuous integration workflow. It will trigger the workflow with any tag matching the semantic version in the form of v1.2.3. However, the workflow will also trigger on pull requests and pushes. We want the continuous integration workflow to execute on pull requests and pushes, but we do not want to execute a release each time. We will need to restrict execution of the release job to only when executing on a tag push.

Restricting release execution#

The first and second goal for the tweeter release workflow is as follows:

  • Triggering the automation when the repository is tagged with a semantic version

  • Running unit tests and validation prior to creating the release

Let's make sure the release job only executes when the repository is tagged:

Job to trigger automation and running unit tests

The preceding job definition completes the first goal of only running the release when a tag starting with v is pushed by specifying an if statement to verify that the github.ref context variable starts with refs/tags/v. The second goal of ensuring the test job executes successfully before attempting to execute the release job is achieved by specifying needs: test. If needs: test was not specified on the release job, both jobs will execute concurrently, which can cause a release to be created without passing validation.

Workspace and environmental setup#

To achieve the rest of the automation goals, we will need to set up the workspace:

Setting up the workspace

The preceding code does the following:

  • Checks out the source at the Git ref associated with the tag.

  • Creates a RELEASE_VERSION environment variable with the tag, such as v1.2.3.

  • Installs Go 1.17 tools.

Building cross-platform binaries and version injection#

The third and fourth goals of the tweeter release flow are as follows:

  • Inject the semantic version of the release into the tweeter application.

  • Build cross-platform versions of the tweeter application

Let's get started by injecting the semantic version of the release into the compiled binary:

Injecting the semantic version into the binary

The preceding steps do the following:

  1. Install the gox command-line tool for simplifying Go cross-compilation.

  2. Build cross-platform binaries for each specified platform/architecture while injecting the RELEASE_VERSION environment variable into a Go ldflag. The ldflag -X replaces the default value of the Version variable in the github.com/devopsforgo/github-actions/pkg/tweeter package with the semantic version tag of the build. The output of gox is structured by OUTPUT_PATH_FORMAT – for example, the output directory looks like the following:

The example output directory
The example output directory

One of the most compelling reasons to use Golang for building applications is the relative ease of building cross-platform, statically linked binaries. With a couple of steps, we can build versions of tweeter for Linux, Windows, macOS targeting AMD64 and ARM64, as well as many other platforms and architectures. These small, statically linked binaries are simple to distribute and execute across platforms and architectures.

With the preceding steps, the release job has compiled the semantic version of the release into the platform and architecture-specific, statically linked binaries. In the next step, we will use the semantic version to generate release notes.

Generating release notes#

We have the following goals associated with generating release notes:

  • Generate release notes from the pull requests in the release.

  • Tag the contributors in the release.

  • Create a GitHub release containing the following:

    • A description containing the generated release notes.

Here's some great news! With a bit of configuration and tagging, release note generation is automatically handled by GitHub. We'll start by adding a new file to the repository, ./.github/release.yml, with the following content:

The release.yml file

The preceding release configuration will tell GitHub to filter and categorize pull requests based on the applied labels. For example, pull requests labeled with ignore-for-release will be excluded from the release notes, but a pull request labeled with enhancement will be grouped under the New Features header in the release notes:

Generating release notes

The preceding step generates release notes. The step executes an API call to the GitHub API to generate the release notes for the given tag. The command captures the JSON body of the response in a tmp-release-notes.json filename. Note that gh requires a GitHub token to interact with the GitHub APIs. The GitHub secret is passed into the GITHUB_TOKEN environment variable and is used by gh to authenticate.

The following is an example of JSON returned from the generate-notes API call:

Example output from the generate-notes API call

We will use tmp-release-notes.json to create the release in the next step.

Creating the GitHub release#

The final goal of creating the release automation is as follows:

  • A title containing the semantic version of the release.

  • A description containing the generated release notes.

  • Artifacts consisting of the cross-platform binaries.

Let's get started creating our release automation:

Creating the release automation

The preceding steps do the following:

  • Execute tar and gzip on the binaries. With Go 1.17, tweeter bins are roughly 6.5 MB. After gzip, each artifact is less than 4 MB.

  • Create a GitHub release using the gh command-line tool, which is available on all GitHub job executors. gh requires a GitHub token to interact with the GitHub APIs. The GitHub secret is passed into the GITHUB_TOKEN environment variable and is used by gh to authenticate. gh release create creates a release and uploads each of the files specified after the arguments. Each file uploaded becomes an artifact on the release. Note # after each artifact file path. The text after # is the name that the artifact will display, as in the GitHub UI. We also specify the title and the release notes using the captured tmp-release-notes.json and jq to parse and select the JSON content.

At this point, we have a created release targeting multiple platforms and architectures, satisfying all our goals for automation. Let's kick off a release and see the results.

Creating a release of tweeter#

We need to first clone our Github repository using the commands below. Replace github_repo with the link to your GitHub repository and repository_name with the name of your repository.

Commands to update the file
Terminal 1
Terminal

Click to Connect...

We then need to replace the .github/workflows/tweeter-automation.yaml file in our cloned directory. Run these commands in the terminal above and then paste the contents of the new file provided just after. You need to then press “Return” and then “Ctrl+C.”

Commands to update the file
The updated file

Now that we have built a release job that will automate the releases of tweeter, we can now tag the repository and release a version of the application.

To start the release automation, we are going to create and push the v0.0.1 tag to the repository by executing the following in the terminal above:

Commands to start release automation

You will have to fill the commands with your own details, as follows:

  • github_email: The email of your GitHub account.

  • github_user: The username of your GitHub account.

In the execution of the final command, you will have to input your GitHub username and access token.

After the tag is pushed, we should be able to go to the Actions tab on our GitHub repository and see the tag workflow executing. If we navigate to the workflow, we should see something like the following:

The workflow job view showing dependent test and release jobs
The workflow job view showing dependent test and release jobs

As we can see in the preceding figure, the tests have been executed and subsequently, the release job has been too. If we navigate to the release job, we should see something like the following:

The release job output view
The release job output view

As we can see in the preceding figure, the release job has successfully executed each of the steps, and the release was created. If we go to the landing page of the repository, we should see that a new release has been created.

With the preceding steps, we have satisfied the goals of our release automation job. We triggered the release job after the tests were executed to ensure a release will always pass our validations before being published. We built statically linked binaries for each of the specified platform/architecture combinations using gox. We leveraged GitHub release notes autogeneration to create beautifully formatted release notes. And finally, we created a release with the generated notes and artifacts from the build.

In this example, we learned to build a release automation job for a Go project, but any language and set of tools can be used in a similar manner to create release automation for any language.

We have no more manual toil to release the tweeter project. All that needs to be done is to push a tag to the repository. Our use of open-source actions has enhanced our ability to author this automation.

Building a Continuous Integration Workflow

Creating a Custom GitHub Action Using Go